#include "PPU.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

////////////////////////////////////////////////////////////////////////////////////////////////////

// scanlines
#define PRE_RENDER_SCANLINE     -1
#define LAST_VISIBLE_SCANLINE   239
#define POST_RENDER_SCANLINE    240
#define LAST_VBLANK_SCANLINE    260
#define LAST_SCANLINE           261

////////////////////////////////////////////////////////////////////////////////////////////////////

PPU::PPU() {
    m_Bus = 0;
    m_Buffer = 0;
    m_Latch = true;
    m_Reg.R0 = 0;
    m_Reg.R1 = 0;
    m_Reg.R2 = 0;
    m_Reg.R3 = 0;
    m_Reg.R4 = 0;
    m_Reg.R5 = 0;
    m_Reg.R6 = 0;
    m_Reg.R7 = 0;
    m_Scanline = -1;
    m_VBlank = false;
    m_SpriteOverflow = false;
    m_NMI = false;
    m_CanRender = false;
    m_LoopyT = 0;
    m_ScreenData = ( unsigned long* )malloc( 256 * 240 * sizeof(unsigned long) );
    memset( m_ScreenData, 0, 256 * 240 * sizeof(unsigned long) );
}

////////////////////////////////////////////////////////////////////////////////////////////////////

PPU::~PPU() {}

////////////////////////////////////////////////////////////////////////////////////////////////////

bool PPU::WantNMI()const {
    return m_NMI;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::ToggleNMI() {
    m_NMI = false;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

unsigned char PPU::ReadRegister( int reg ) {
    switch( reg ) {
        case 0: {
            return m_Bus;
        } break;

        case 1: {
            return m_Bus;
        } break;

        case 2: {
            return Read2002();
        } break;

        case 3: {
            return m_Bus;
        } break;

        case 4: {
            return Read2004();
        } break;

        case 5: {
            return m_Bus;
        } break;

        case 6: {
            return m_Bus;
        } break;

        case 7: {
            return Read2007();
        } break;

        default: {
            printf( "\nERROR: < %i > no es un registro de PPU valido (lectura)", reg );
            getchar();
            exit( 0 );
        } break;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::WriteRegister( int reg, unsigned char value ) {
    switch( reg ) {
        // TODO
        case 0: {
            m_Bus = value;
            Write2000( value );
        } break;

        case 1: {
            m_Bus = value;
            m_Reg.R1 = value;
        } break;

        case 2: {
            m_Bus = value;
        } break;

        case 3: {
            m_Bus = value;
            Write2003( value );
        } break;

        case 4: {
            m_Bus = value;
            Write2003( value );
        } break;

        case 5: {
            m_Bus = value;
            Write2005( value );
        } break;

        case 6: {
            m_Bus = value;
            Write2006( value );
        } break;

        case 7: {
            m_Bus = value;
            Write2007( value );
        } break;

        default: {
            printf( "\nERROR: < %i > no es un registro de PPU valido (escritura)", reg );
            getchar();
            exit( 0 );
        } break;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////

unsigned char PPU::Read2002() {
    unsigned char t = m_Reg.R2;

    if( m_VBlank ) t |= 0x80;
    if( m_SpriteOverflow ) t |= 0x20;

    m_Reg.R2 &= 0x7f;
    m_Latch = true;

    return t;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

unsigned char PPU::Read2004() {
    // FIXME: no se si esto es asi o al reves, o si es del todo correcto...
    if( m_VBlank ) {
        return m_Reg.R3;
    } else {
        return m_ReadSRAM( m_Reg.R3 );
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////

unsigned char PPU::Read2007() {
    unsigned char t = 0;

    // la direccion corresponde a la paleta
    if( m_Reg.R6 < 0x3f00 ) {
        t = m_Buffer;
        m_Buffer = m_ReadVRAM( m_Reg.R6 );
    }

    // la direccion no corresponde a la paleta
    else if( m_Reg.R6 < 0x4000 ) {
        t = m_ReadVRAM( m_Reg.R6 );
        m_Buffer = m_ReadVRAM( (m_Reg.R6 - 0x1000) & 0xffff );
    }

    m_Reg.R6 += ( TestBit(m_Reg.R0, 2) ? 32 : 1 );

    return t;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::Write2000( unsigned char value ) {
    m_Reg.R0 = value;
    m_LoopyT &= 0x73ff;
    m_LoopyT |= ( (value & 0x03) << 10 );
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::Write2003( unsigned char value ) {
    m_Reg.R3 = value;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::Write2004( unsigned char value ) {
    // FIXME: puede que el valor de OAMADDR no se modifique si se escribe cuando la PPU esta renderizando
    m_WriteSRAM( m_Reg.R3++, value );
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::Write2005( unsigned char value ) {
    if( m_Latch ) {
        m_LoopyT &= 0x7fe0;
        m_LoopyT |= ( (value & 0xf8) >> 3 );
    } else {
        m_LoopyT &= 0x0c1f;
        m_LoopyT |= ( ((value & 0xf8) << 2) | ((value & 0x07) << 12) );
    }
    m_Latch ^= true;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::Write2006( unsigned char value ) {
    if( m_Latch ) {
        m_LoopyT &= 0x40ff;
        m_LoopyT |= ( (value & 0x3f) << 8 );
    } else {
        m_LoopyT &= 0x7f00;
        m_LoopyT |= value;
        m_Reg.R6 = m_LoopyT;
    }
    m_Latch ^= true;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::Write2007( unsigned char value ) {
    // name tables
    if( m_Reg.R6 < 0x3000 ) {
        // TODO: mappers
        if( m_Reg.R6 >= 0x2000 ) {
            unsigned short a = ( m_Reg.R6 & 0x2c00 );

            // mirroring vertical
            if( m_Mirroring == VERTICAL ) {
                if( a == 0x2000 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 + 0x0800) & 0xffff, value );
                } else if( a == 0x2400 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 + 0x0800) & 0xffff, value );
                } else if( a == 0x2800 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 - 0x0800) & 0xffff, value );
                } else if( a == 0x2c00 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 - 0x0800) & 0xffff, value );
                }
            }

            // mirroring horizontal
            else if( m_Mirroring == HORIZONTAL ) {
                if( a == 0x2000 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 + 0x0400) & 0xffff, value );
                } else if( a == 0x2400 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 - 0x0400) & 0xffff, value );
                } else if( a == 0x2800 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 + 0x0400) & 0xffff, value );
                } else if( a == 0x2c00 ) {
                    m_WriteVRAM( m_Reg.R6, value ); m_WriteVRAM( (m_Reg.R6 - 0x0400) & 0xffff, value );
                }
            }
        }
    }

    
    // paleta
    else if( m_Reg.R6 < 0x4000 ) {
        value &= 0x3f;
        m_WriteVRAM( m_Reg.R6, value );
        if( (m_Reg.R6 == 0x3f00) || (m_Reg.R6 == 0x3f10) ) {
            m_WriteVRAM( 0x3f00, value );
            m_WriteVRAM( 0x3f10, value );
        } else if( (m_Reg.R6 == 0x3f04) || (m_Reg.R6 == 0x3f14) ) {
            m_WriteVRAM( 0x3f04, value );
            m_WriteVRAM( 0x3f14, value );
        } else if( (m_Reg.R6 == 0x3f08) || (m_Reg.R6 == 0x3f18) ) {
            m_WriteVRAM( 0x3f08, value );
            m_WriteVRAM( 0x3f18, value );
        } else if( (m_Reg.R6 == 0x3f0c) || (m_Reg.R6 == 0x3f1c) ) {
            m_WriteVRAM( 0x3f0c, value );
            m_WriteVRAM( 0x3f1c, value );
        }
    }

    m_Reg.R6 += ( TestBit(m_Reg.R0, 2) ? 32 : 1 );
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::WriteSpriteDMA( std::list<unsigned char> data ) {
    unsigned char address = 0;
    for( std::list<unsigned char>::iterator value = data.begin(); value != data.end(); value++ ) m_WriteSRAM( address++, *value );
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::RunScanline() {
    if( m_Scanline <= PRE_RENDER_SCANLINE ) {
        // (al menos por lo pronto) nada...
        // en el primer scanline, si el background o los sprites estan habilitados, LoopyT se copia a PPU[6]
        if( TestBit(m_Reg.R1, 3) || TestBit(m_Reg.R1, 4) ) m_Reg.R6 = m_LoopyT;
    } else if( m_Scanline <= LAST_VISIBLE_SCANLINE ) {
        // al principio de cada scanline visible, si el background o los sprites estan habilitados, LoopyT se copia a PPU[6] y se resetean los
        // bits de scrolling relativos al eje X (los 5 mas bajos de de PPU[6]) y el bit asociado a la Name Table (el bit 10 de PPU[6])

        if( TestBit(m_Reg.R1, 3) || TestBit(m_Reg.R1, 4) ) {
            // FIXME: que onda con esto??? 'nes_emu' primero dice que hay que se hace al principio de cada scanline, y poco mas adelante dice que
            // se hace al final de cada una...
            m_Reg.R6 &= 0xfbe0;
            m_Reg.R6 |= ( m_LoopyT & 0x041f );

            // dibujar en el buffer de salida el background y los sprites
            DrawSprites( false );
            DrawBackground();
            DrawSprites( true );

            // FIXME: no me termina de quedar del todo claro, pero esto es mas o menos lo que entendi...
            // al final de cada scanline visible se incremente el scrolling en el eje Y (los bits 12, 13 y 14 de PPU[6]). Cuando estos bits sean
            // sinmultaneamente 1 se habran renderizado 8 scanlines (111b = 7d, o sea 8 scanlines). En este punto, se desea saber si las 240 scanlines
            // visibles ya fueron renderizadas. Para ello, se chequea lo que se denomina 'y-offset', que corresponde a los bits 5, 6, 7, 8 y 9 de PPU[6]; si
            // estos son sinmultaneamente 1, entonces se habran renderizado los 8 scanlines de 32 posibles tiles (11111b = 31d, o sea 32 tiles). En terminos normales,
            // verticalmente solo entran 30 tiles, pero puede que eventualmente se forzarsen 32 tiles mediante programacion.
            // Una vez que el y-offset llega a 29 (o sea los 30 tiles verticales), entonces se reseta el scrolling en Y y se alterna el bit asociado a la
            // Name Table. Por otro lado, si el valor llegase eventualmente a 31, entonces se resea el scrolling en Y *PERO* sin alternar el bit asociado a la Name Table.

            if( (m_Reg.R6 & 0x7000) == 0x7000 ) { // cada 8 scanlines...
                unsigned short t = ( m_Reg.R6 & 0x03e0 ); // valor del y-offset
                m_Reg.R6 &= 0x0fff; // resetear el scrolling en Y

                switch( t ) { // chequear el y-offset
                    case 0x03a0: { // si es 28...
                        m_Reg.R6 ^= 0x0ba0;
                    } break;

                    case 0x03e0: { // si es 30...
                        m_Reg.R6 ^= 0x03e0;
                    } break;

                    default: { // si no estamos ante ninguno de los dos casos anteriores, aumentamos el scrolling en Y normalmente
                        m_Reg.R6 += 0x0020;
                    } break;
                }
            } else { // si aun no se llego a los 8 scanlines, se incrementa el Fine-Y
                m_Reg.R6 += 0x1000;
            }
        }

        if( m_Scanline == LAST_VISIBLE_SCANLINE ) m_CanRender = true;
    } else if( m_Scanline <= POST_RENDER_SCANLINE ) {
        // el VBlank comienza al final de este scanline
        m_CanRender = false;
        m_VBlank = true;
        if( TestBit(m_Reg.R0, 7) ) RaiseInterrupt( NMI );
    } else if( m_Scanline <= LAST_VBLANK_SCANLINE ) {
        // en VBlank, PPU descansando...
    } else if( m_Scanline <= LAST_SCANLINE ) {
        // fin del VBlank
        m_VBlank = false;
        m_Scanline = -2; // es -2, y no -1, porque estamos contando los scanlines desde -1 (en lugar de hacerlo desde 0)
    }

    m_Scanline++;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::DrawBackground() {
    for( int tile = 0; tile < 32; tile++ ) {
        unsigned short tile_address = ( 0x2000 | (m_Reg.R6 & 0x0fff) );
        unsigned short pt_address = ( (m_ReadVRAM(tile_address) << 4) | (m_Reg.R6 >> 12) | ((TestBit(m_Reg.R0, 4) ? 1 : 0) << 12) );
        unsigned char at_byte =  m_ReadVRAM( 0x23c0 | (m_Reg.R6 & 0x0c00) | ATTRIBUTE_LOCK[m_Reg.R6 & 0x03ff] );
        at_byte &= ATTRIBUTE_MASK[ m_Reg.R6 & 0x03ff ];
        at_byte >>= ATTRIBUTE_SHIFT[ m_Reg.R6 & 0x03ff ];
        for( int bit = 7; bit >= 0; bit-- ) {
            unsigned char index = ( (at_byte << 2) | (((m_ReadVRAM(pt_address | 8) >> bit) & 0x01) << 1) | ((m_ReadVRAM(pt_address) >> bit) & 0x01) );
            m_ScreenData[ m_Scanline * 256 + tile * 8 + (bit ^ 7) ] = PALETTE[ m_ReadVRAM(0x3f00 | index) ];
        }

        // al final de cada tile renderizado, se incrementa el scrolling en el eje X. Cuando el valor del scrolling en X llegue a 0x1f (31d, lo que implica
        // 32 tiles) se habra renderizado un scanline completo. Entonces, se reseta el scrolling en X y se alterna el bit asociado a la Name Table.
        if( (m_Reg.R6 & 0x001f) == 0x001f ) m_Reg.R6 ^= 0x041f;
        else                                m_Reg.R6++;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::DrawSprites( bool priority ) {
    int counter = 0;
    m_SpriteOverflow = false;
    for( int i = 252; i >= 0; i -= 4 ) {
        unsigned char b0 = m_ReadSRAM( i ); // coordenada Y
        unsigned char b1 = m_ReadSRAM( (i + 1) & 0xff );
        unsigned char b2 = m_ReadSRAM( (i + 2) & 0xff );
        unsigned char b3 = m_ReadSRAM( (i + 3) & 0xff ); // coordenada X
    
        unsigned char range = ( (m_Scanline - 21 - b0 + (TestBit(m_Reg.R0, 5) ? 16 : 8)) & 0xff );
        bool in_range = ( (range >= 0) && (range <= 7) );
        
        // 0 => delante del background; 1 => detras del background
        if( in_range && (TestBit(b2, 5) == priority) ) {
            if( ++counter > 8 ) m_SpriteOverflow = true;
            // FIXME: por el momento solo implemento los sprites de 8x8; corregir al implementar sprites de 8x16.
            if( ! TestBit(m_Reg.R0, 5) ) {
                unsigned char line = ( m_Scanline % 8 );

                // flip vertical => TestBit( b2, 7 )
                unsigned char pt_entry_l = m_ReadVRAM( (TestBit(m_Reg.R0, 3) ? 0x1000 : 0x0000) | ((b1 << 4) & 0xffff) | (TestBit(b2, 7) ? 7 - line : line) );
                unsigned char pt_entry_h = m_ReadVRAM( (TestBit(m_Reg.R0, 3) ? 0x1000 : 0x0000) | ((b1 << 4) & 0xffff) | (TestBit(b2, 7) ? 7 - line : line) | 8 );

                if( TestBit(b2, 6) ) { // flip horizontal
                    for( int k = 0; k < 8; k++ ) {
                        unsigned char index = ( ((((pt_entry_h >> k) & 0x01) << 1) | ((pt_entry_l >> k) & 0x01)) & 0xff );
                        if( index ) {
                            index = ( (((b2 & 0x03) << 2) | index ) & 0xff );
                            if( (b0 + line + 1) * 256 + b3 < 256 * 240 ) m_ScreenData[ (b0 + line + 1) * 256 + b3 ] = PALETTE[ m_ReadVRAM(0x3f10 | index) ];
                        }
                        b3++;
                    }
                } else {
                    for( int k = 7; k >= 0; k-- ) {
                        unsigned char index = ( ((((pt_entry_h >> k) & 0x01) << 1) | ((pt_entry_l >> k) & 0x01)) & 0xff );
                        if( index ) {
                            index = ( (((b2 & 0x03) << 2) | index ) & 0xff );
                            if( (b0 + line + 1) * 256 + b3 < 256 * 240 ) m_ScreenData[ (b0 + line + 1) * 256 + b3 ] = PALETTE[ m_ReadVRAM(0x3f10 | index) ];
                        }
                        b3++;
                    }
                }
            }
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////

void PPU::RaiseInterrupt( Interrupt i ) {
    if( i == NMI ) m_NMI = true;
}

////////////////////////////////////////////////////////////////////////////////////////////////////

bool PPU::WantRender() {
    return m_CanRender;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
